package tools;

import java.io.Closeable;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import entity.DefinedRuntimeException;

/**
 * 封装写日志操作的实体类. <br/>
 * 类中的方法不会检查路径是否可用， 调用者请保证传入的路径的父文件夹均存在并可用。 <br/>
 * 对象使用完毕后应当关闭。
 * @author Rex
 *
 */
public class WriteLogOperation implements Closeable {
	
	/**
	 * 保存待写出消息详情的实体类.
	 * @author Rex
	 *
	 */
	private class Detail {
		
		private String log;
		private Throwable cause;
		
		/**
		 * 发生时间.
		 */
		private long occurrenceTime;
		
		/**
		 * 状态.
		 * 共三种状态：
		 * 0=只有String和occurrenceTime信息；
		 * 1=只有Throwable和occurrenceTime信息；
		 * 2=有String、Throwable和occurrenceTime信息；
		 */
		private int status;

		private Detail(String log, long occurrenceTime) {
			this.log = log;
			this.occurrenceTime = occurrenceTime;
			this.status = 0;
		}

		private Detail(Throwable cause, long occurrenceTime) {
			this.cause = cause;
			this.occurrenceTime = occurrenceTime;
			this.status = 1;
		}

		private Detail(String log, Throwable cause, long occurrenceTime) {
			this.log = log;
			this.cause = cause;
			this.occurrenceTime = occurrenceTime;
			this.status = 2;
		}

		private String getLog() {
			return log;
		}
		private Throwable getCause() {
			return cause;
		}
		private long getOccurrenceTime() {
			return occurrenceTime;
		}
		private int getStatus() {
			return status;
		}
		
	}
	
	/**
	 * 文件输出流.
	 */
	private FileWriter writer = null;
	
	/**
	 * 一个阻塞队列，线程安全.
	 */
	private LinkedBlockingQueue<Detail> queue  = null;
	
	/**
	 * 标记是否需要启动后台线程.<br/>
	 * 同时可用作标记后台线程是否已启动（当此布尔值为false时，表示后台线程已启动）。<br/>
	 * 启动后台线程是为了获取阻塞队列的头部元素用以写出到磁盘文件。
	 */
	private boolean flag = true;
	
	/**
	 * 标记是否继续执行后台线程.
	 */
	private volatile boolean run = true;
	
	/**
	 * 后台线程.
	 * 用于获取阻塞队列的头部元素并写出到磁盘文件。
	 */
	private Thread t = null;

	/**
	 * 用于格式化当前系统时间.
	 */
	private final SimpleDateFormat sdf = new SimpleDateFormat("发生于 -> yyyy-MM-dd HH:mm:ss");

	/**
	 * 换行符.
	 */
	private final String line_break = "\r\n";

	/**
	 * 空格.
	 */
	private final String blank = " ";

	/**
	 * 提示出现了一个null.
	 */
	private final String null_obj = "(null)";

	/**
	 * 写出日志的路径. 
	 * @see WriteLogOperation#setPath(String)
	 */
	private String path;

	/**
	 * 每条记录是否添加时间后缀. 默认为true。
	 * @see WriteLogOperation#setAddSuffix(boolean)
	 */
	private boolean addSuffix = true;

	/**
	 * 默认的日志写出路径：
	 * Windows环境下默认为C:\temp\error.log，
	 * Linux/Unix环境下默认为/usr/error.log.
	 * @throws DefinedRuntimeException
	 */
	public WriteLogOperation() throws DefinedRuntimeException {
		if (System.getProperty("os.name").toLowerCase().contains("windows")) {
			// Windows
			File file = new File("C:\\temp");
			if (file.exists() == false) {
				file.mkdir();
			}
			path = "C:\\temp\\error.log";
		} else {
			// Linux/Unix
			path = "/usr/error.log";
		}
		createStream();
	}
	
	/**
	 * 默认的日志写出路径：
	 * Windows环境下默认为C:\temp\error.log，
	 * Linux/Unix环境下默认为/usr/error.log.
	 * @param addSuffix 是否在每条日志末尾增加时间后缀，默认为true
	 * @throws DefinedRuntimeException
	 */
	public WriteLogOperation(boolean addSuffix) throws DefinedRuntimeException {
		this();
		this.addSuffix = addSuffix;
	}

	/**
	 * 构造器不会检查路径是否可用， 调用者请保证传入的路径存在并可用.
	 * @param path
	 * @throws DefinedRuntimeException
	 */
	public WriteLogOperation(String path) throws DefinedRuntimeException {
		this.path = path;
		createStream();
	}
	
	/**
	 * 构造器不会检查路径是否可用， 调用者请保证传入的路径存在并可用.
	 * @param path
	 * @param addSuffix 是否在每条日志末尾增加时间后缀，默认为true
	 * @throws DefinedRuntimeException
	 */
	public WriteLogOperation(String path, boolean addSuffix) throws DefinedRuntimeException {
		this(path);
		this.addSuffix = addSuffix;
	}

	/**
	 * 得到日志写出路径.
	 * @see WriteLogOperation#path
	 * @return 日志路径
	 */
	public String getPath() {
		return path;
	}

	/**
	 * 得到是否添加时间后缀的布尔值.
	 * @see WriteLogOperation#addSuffix
	 * @return
	 */
	public boolean isAddSuffix() {
		return addSuffix;
	}

	/**
	 * 设置是否添加时间后缀.
	 * @see WriteLogOperation#addSuffix
	 * @param addSuffix
	 */
	public void setAddSuffix(boolean addSuffix) {
		this.addSuffix = addSuffix;
	}
	
	/**
	 * 真正将消息写出到磁盘的方法.
	 * 此方法只负责写字符串。
	 * @param msg 此方法不判空，调用者应保证传入的字符串不为空
	 */
	private void writeDown(String msg) throws DefinedRuntimeException {
		try {
			// 若有此文件，则直接使用；若无此文件，则先新建
			writer.write(msg);
			writer.flush();
		} catch (IOException e) {
			// 只抛出异常，不写日志
			throw new DefinedRuntimeException(false, "写出日志失败，文件输出流异常", e);
		} catch (Exception e) {
			// 只抛出异常，不写日志
			throw new DefinedRuntimeException(false, "写日志遭遇未知异常", e);
		}
	}

	/**
	 * 写出日志到磁盘文件.
	 * @param log
	 * @throws DefinedRuntimeException
	 */
	public void write(String log) throws DefinedRuntimeException {
		writeDown(createMsg(log));
	}
	
	/**
	 * 写出日志到磁盘文件.
	 * @param cause
	 * @throws DefinedRuntimeException
	 */
	public void write(Throwable cause) throws DefinedRuntimeException {
		writeDown(createMsg(getInfo(cause)));
	}

	/**
	 * 写出日志到磁盘文件.
	 * @param log
	 * @param cause
	 * @throws DefinedRuntimeException
	 */
	public void write(String log, Throwable cause) throws DefinedRuntimeException {
		writeDown(createMsg(getInfo(log, cause)));
	}
	
	/**
	 * 写出日志到磁盘文件（同步）.
	 * @param log
	 * @throws DefinedRuntimeException
	 */
	public void writeSync(String log) throws DefinedRuntimeException {
		put(new Detail(log, addSuffix ? System.currentTimeMillis() : 0));
	}

	/**
	 * 写出日志到磁盘文件（同步）.
	 * @param cause
	 * @throws DefinedRuntimeException
	 */
	public void writeSync(Throwable cause) throws DefinedRuntimeException {
		put(new Detail(cause, addSuffix ? System.currentTimeMillis() : 0));
	}

	/**
	 * 写出日志到磁盘文件（同步）.
	 * @param log
	 * @param cause
	 * @throws DefinedRuntimeException
	 */
	public void writeSync(String log, Throwable cause) throws DefinedRuntimeException {
		put(new Detail(log, cause, addSuffix ? System.currentTimeMillis() : 0));
	}

	/**
	 * 将异常信息转换为字符串.
	 * @param cause
	 * @return
	 */
	private String getInfo(Throwable cause) {
		String str = null_obj;
		if (cause != null) {
			try (StringWriter sw = new StringWriter(); 
					PrintWriter pw = new PrintWriter(sw)) {

				cause.printStackTrace(pw);
				str = sw.toString();
			} catch (Exception e) {
				str = cause.getClass().getName();
			}
		}
		return str;
	}

	/**
	 * 将异常信息转换为字符串.
	 * @param log
	 * @param cause
	 * @return
	 */
	private String getInfo(String log, Throwable cause) {
		return new StringBuilder(log == null ? null_obj : log).append(line_break).append(getInfo(cause))
				.toString();
	}
	
	/**
	 * 将异常信息转换为字符串.
	 * @param log
	 * @param cause
	 * @return
	 */
	private String getInfo(Detail detail) {
		String str = null_obj;

		switch (detail.getStatus()) {
		// 只有String和occurrenceTime信息；
		case 0:
			str = detail.getLog();
			break;

		// 只有Throwable和occurrenceTime信息；
		case 1:
			str = getInfo(detail.getCause());
			break;

		// 有String、Throwable和occurrenceTime信息；
		case 2:
			str = getInfo(detail.getLog(), detail.getCause());
			break;

		default:
			break;
		}

		str = new StringBuilder(str).append(blank)
				.append(addSuffix ? sdf.format(new Date(detail.getOccurrenceTime())) : "").append(line_break)
				.toString();
		
		return str;
	}

	/**
	 * 添加元素到队列.
	 * @param log
	 * @throws DefinedRuntimeException
	 */
	private void put(Detail detail) throws DefinedRuntimeException {
		try {
			// 初次调用此方法时，初始化queue
			if (queue == null) {
				queue = new LinkedBlockingQueue<>();
			}
			// 将待写出的消息保存在队列
			queue.put(detail);
			// 启动线程
			// 后一个判断条件是为了保证if语句块在queue初始化后才执行
			if (flag && queue != null) {
				flag = false;
				//启动一个线程用以获取阻塞队列的头部元素并写出到磁盘文件
				t = new Thread(() -> start());
				// 设置为守护线程
				t.setDaemon(true);
				t.start();
			}
		} catch (InterruptedException e) {
			// 只抛出异常，不写日志
			throw new DefinedRuntimeException(false, "WriteLogOperation.put(String)异常", e);
		}
	}

	/**
	 * 开始循环获取阻塞队列的头部元素并写出到磁盘文件.
	 */
	private void start() {
		try {
			while (run) {
				writeDown(getInfo(queue.take()));
			}
		} catch (Exception e) {
			start();
		}
	}

	/**
	 * 新建文件输出流.
	 * @throws DefinedRuntimeException
	 */
	private void createStream() throws DefinedRuntimeException {
		try {
			writer = new FileWriter(path, true);
		} catch (IOException e) {
			// 只抛出异常，不写日志
			throw new DefinedRuntimeException(false, "新建文件输出流异常", e);
		}
	}
	
	/**
	 * 将传入的消息制作成合乎要求的格式，以待后续方法写出.
	 * @param log
	 * @return
	 */
	private String createMsg(String log) {
		return new StringBuilder(log == null ? null_obj : log).append(blank)
				.append(addSuffix ? sdf.format(new Date()) : "").append(line_break).toString();
	}
	
	/**
	 * 关闭对象.
	 * @throws IOException
	 */
	@Override
	public void close() throws IOException {
		if (flag == false) {
			run = false;
			queue.clear();
			t.interrupt();
		}
		writer.close();
	}
	
}